前七天我們把 Python 的基本骨架都走過一遍了:變數、判斷、迴圈、List、Dictionary、Function。到這裡為止,我們寫的程式有一個共同特徵——只要有一個地方出錯,整支程式就直接停掉。
這在練習的時候沒什麼感覺,反正改一改再跑一次就好。但如果把場景換成後面要做的「生活助理」,情況就完全不一樣了:
使用者說「幫我安排明天的行程」,助理去抓天氣 API,結果剛好網路斷線。
如果程式直接崩潰,使用者看到的就是一大串紅字,而不是「抱歉,我現在拿不到天氣資料,先幫你排其他的」。
一個會自主做事的 Agent,最重要的能力之一就是在事情出錯時還能繼續往下走。今天就來處理這件事。
Python 的錯誤大致可以分成兩種:
1. 語法錯誤(SyntaxError):你程式碼寫錯了,Python 根本讀不懂,連跑都跑不起來。
if x = 5: # 應該是 ==
print("hi")
這種錯誤只能自己改,沒得救。
2. 執行時錯誤(Exception):程式碼本身是合法的,但執行到某一行的時候出事了。
numbers = [1, 2, 3]
print(numbers[10]) # IndexError:索引超出範圍
data = {"name": "Alice"}
print(data["age"]) # KeyError:字典裡沒有這個 key
print(int("abc")) # ValueError:字串轉不成數字
print(10 / 0) # ZeroDivisionError:除以零
今天要處理的是第二種。這類錯誤的共同點是:它跟「當下的資料」有關,不是跟「程式碼」有關。而 Agent 面對的資料永遠來自外部(使用者輸入、API 回應、檔案內容),所以這種錯誤是必然會發生的。
基本語法長這樣:
try:
# 有可能出錯的程式碼
result = 10 / 0
except ZeroDivisionError:
# 出錯時要做的事
print("不能除以零喔")
執行流程是:
try 區塊裡的程式碼。except,繼續往下。except 後面寫的,就跳到 except 區塊執行。我們把 Day 2 練習過的「讓使用者輸入數字」改寫看看:
def ask_number(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("請輸入數字,例如 25")
age = ask_number("請輸入年齡:")
print(f"你今年 {age} 歲")
以前使用者只要不小心打了「二十五」,程式就爆掉。現在它會禮貌地再問一次。
這個「出錯就重問」的模式,其實就是 Agent 最原始的**重試(retry)**邏輯。
一段程式碼可能有很多種出錯方式,我們可以寫多個 except:
def divide(a, b):
try:
return int(a) / int(b)
except ValueError:
print("輸入的不是數字")
return None
except ZeroDivisionError:
print("除數不能是 0")
return None
print(divide("10", "2")) # 5.0
print(divide("10", "abc")) # 輸入的不是數字 → None
print(divide("10", "0")) # 除數不能是 0 → None
Python 會由上往下比對,第一個符合的 except 會被執行,其他就跳過。
如果你想一次抓多種:
except (ValueError, ZeroDivisionError):
print("輸入有問題")
完整的結構其實有四個部分:
def read_config(path):
try:
f = open(path, "r", encoding="utf-8")
content = f.read()
except FileNotFoundError:
print(f"找不到檔案:{path}")
return None
else:
# 沒有出錯才會執行
print("讀取成功")
return content
finally:
# 不管有沒有出錯,都一定會執行
print("這行一定會跑")
else:try 區塊沒有出錯時才執行。finally:不管如何都會執行,通常用來做「收尾」,例如關閉檔案、關閉連線。對 Agent 來說,finally 特別有用——不管這次工具呼叫成功或失敗,我們都想把「這次做了什麼」記錄下來。
只印「出錯了」對除錯沒什麼幫助。我們可以把錯誤物件接起來:
try:
value = int("abc")
except ValueError as e:
print(f"發生錯誤:{e}")
# 發生錯誤:invalid literal for int() with base 10: 'abc'
as e 把錯誤物件綁到變數 e,印出來就是 Python 給的錯誤訊息。
如果你想抓「任何錯誤」,可以用 Exception:
try:
do_something()
except Exception as e:
print(f"未預期的錯誤:{type(e).__name__}: {e}")
⚠️ 但要小心:
except Exception會把所有錯誤都吞掉,包括你自己寫錯的 bug。平常盡量寫明確的錯誤型態,只有在「最外層」(例如整個 Agent 的主迴圈)才用它當最後一道防線。
有時候「合法但不合理」的情況,Python 不會報錯,但我們希望它報錯:
def set_temperature(celsius):
if celsius < -273.15:
raise ValueError("溫度不能低於絕對零度")
return celsius
raise 會主動製造一個錯誤。這在寫 Function 的時候很重要:與其讓錯誤的資料默默往下流,不如在源頭就擋下來。
搭配 Day 7 的 Function 概念,一個「好工具」應該要有明確的邊界檢查:
def get_weather(city):
if not isinstance(city, str) or not city.strip():
raise ValueError("city 必須是非空字串")
# ...實際查詢邏輯
現在把今天的東西放進 Agent 的脈絡裡。
一個 Agent 在執行任務時,大致是這樣的流程:
使用者輸入 → 決定要用哪個工具 → 執行工具 → 拿到結果 → 回覆
每一個箭頭都可能出錯:
| 階段 | 可能的錯誤 | 沒處理會怎樣 |
|---|---|---|
| 使用者輸入 | 空字串、亂打 | 後面全部爛掉 |
| 決定工具 | 模型給了不存在的工具名 | KeyError,程式停掉 |
| 執行工具 | 網路斷線、API 額度用完 | 整個對話中斷 |
| 解析結果 | 回傳的不是預期格式 | JSONDecodeError |
所以我們希望的不是「不會出錯」,而是「出錯時 Agent 知道自己出錯了,並且能把這件事變成下一步的判斷依據」。
用今天學到的東西,可以先寫出一個雛形:
def run_tool(tool_name, tools, arguments):
"""執行工具,並把錯誤轉換成可以回報給模型的文字。"""
if tool_name not in tools:
return {"ok": False, "error": f"沒有名為 {tool_name} 的工具"}
try:
result = tools[tool_name](**arguments)
except ValueError as e:
return {"ok": False, "error": f"參數有問題:{e}"}
except Exception as e:
return {"ok": False, "error": f"工具執行失敗:{type(e).__name__}: {e}"}
else:
return {"ok": True, "result": result}
# 用法
def get_weather(city):
if city == "火星":
raise ValueError("這個城市不在地球上")
return f"{city} 今天晴天 26 度"
tools = {"get_weather": get_weather}
print(run_tool("get_weather", tools, {"city": "台北"}))
# {'ok': True, 'result': '台北 今天晴天 26 度'}
print(run_tool("get_weather", tools, {"city": "火星"}))
# {'ok': False, 'error': '參數有問題:這個城市不在地球上'}
print(run_tool("查星座", tools, {}))
# {'ok': False, 'error': '沒有名為 查星座 的工具'}
注意這裡的關鍵設計:錯誤沒有讓程式停下來,而是變成一段可以被閱讀的文字。
後面(Day 21、22)我們會看到,這段文字可以原封不動地丟回給 AI,讓它知道「剛剛那個工具失敗了,原因是 XXX」,然後它可以自己決定要換一個工具、換一組參數,還是直接告訴使用者做不到。
這就是「自主決策」的起點——Agent 要能看見失敗,才有辦法繞過失敗。
試著把 Day 6 寫過的「待辦清單」加上例外處理:
todos = []
def add_todo(title, priority):
if not title.strip():
raise ValueError("標題不能是空的")
if priority not in ("高", "中", "低"):
raise ValueError(f"優先度只能是 高/中/低,你給的是 {priority}")
todos.append({"title": title, "priority": priority, "done": False})
return todos[-1]
for title, priority in [("買牛奶", "高"), ("", "中"), ("運動", "超高")]:
try:
item = add_todo(title, priority)
print(f"已新增:{item['title']}")
except ValueError as e:
print(f"新增失敗:{e}")
print(f"\n目前有 {len(todos)} 筆待辦")
輸出:
已新增:買牛奶
新增失敗:標題不能是空的
新增失敗:優先度只能是 高/中/低,你給的是 超高
目前有 1 筆待辦
三筆資料裡有兩筆有問題,但程式完整跑完了。這就是我們要的。
今天講的是一個很基礎、但常常被略過的東西:例外處理。
try / except 讓程式在出錯時能繼續往下走else / finally 幫你把「成功才做」和「一定要做」分開raise 讓你在源頭主動擋下不合理的資料明天要講檔案讀寫——讓程式的記憶不再只存在於記憶體裡,關掉之後還記得住。這也是 Agent 長期記憶的第一步。
我們明天見。